跳到主要内容

跨源资源共享 CORS 和 Csrf 的概念

跨域资源共享(CORS)

跨域:A域名访问 B域名的数据(域名或者请求端口或者协议不一致的时候就跨域了)

www.example.com              www.example.cn          跨域
www.example.com:8080 www.example.com:8081 跨域
www.example.com example.cn 跨域
http://www.example.com https://www.example.cn 跨域

什么是 CORS?

CORS 全称是 Cross-Origin Resource Sharing,直译过来就是跨域资源共享。要理解这个概念就需要知道 资源同源策略 这三个概念。

  • 域,指的是一个站点,由 protocolhostport 三部分组成,其中 host 可以是域名,也可以是 ipport 如果没有指明,则是使用 protocol 的默认端口
  • 资源,是指一个 URL 对应的内容,可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容
  • 同源策略,指的是为了防止 XSS,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。

了解了这三个概念,我们就能理解为什么有 CORS 规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS

如下,就是没有同源策略导致的

20221014153902

当用户登录受信任网站 A 时,A 会在本地生成 Cookie,这时用户如果在不登出 A 的情况下,访问危险网站 B,B 就可以拿到 A 给用户的 Cookie 再给 A 发请求

什么是 Csrf?

CSRF,跨站请求伪造。攻击者在受害者未知的情况下可以使用受害者的认证发送伪造请求给目标站点。

提示

从字面上看,CSRF 是一种攻击方式。而CORS是一种资源共享的方式。

所以为了防止被 CSRF 攻击,引入了同源策略这个机制,同源策略主要是防止客户端这块被劫持,而非服务端。如果需要跨域访问时就在请求头加上的这个域名的字段列表

出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件

警告

对于 浏览器限制 这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了

提示

注意:跨域不只可以用 CORS 还可以使用 JSONP 的方式,但是 JSONP 只是一种绕过浏览器的方式,有点投机取巧且不安全。所以一般使用 CORS 这个规范(CORS 是一个 W3C 标准,全称是 "跨域资源共享")

同源策略详情

同源策略是一个重要的安全策略,它用于限制一个源的网站或者它加载的脚本如何能与另一个源的资源进行交互。同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据

设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

怎么判别是同源?

如果两个 URL 的 协议、端口 (如果有指定的话)和 域名 都相同的话,则这两个 URL 是同源

即便两个不同的域名指向同一个ip地址,也非同源。

a9RInf.png

同源策略有什么限制?

  • Cookie、LocalStorage、IndexedDB 等存储性内容 例如:A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"
  • DOM 节点 无法获得
  • AJAX 请求不能发送

但是有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

注:之所以要贴下面这个是因为某些答案一本正经的胡说八道,顶级域 != 顶级域名

a9odaV.png

顶级域比如 .com .org .cn 但是注意了,顶级域 != 顶级域名 一级域名又称顶级域名 例如:baidu.com zhihu.com qq.com

未配置 CORS 的例子

页面端:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h1>返回测试页面</h1>
<script>
fetch("http://localhost:3001/")
.then(res => {
return res.text();
})
.then(res => {
alert(res);
});
</script>
</body>

</html>

服务器端:

const express = require('express');

// 3000端口的服务 将当前目录作为http服务
let app = express();
// 注意,当静态目录下有 index.html 默认将这个html返回出去
app.use(express.static(__dirname))
app.listen(3000)

// 3001端口的服务 返回数据
let app2 = express();
app2.get('/', (rep, res) => {
res.send("你好")
})
app2.listen(3001)

出现如下报错

aCNFun.png

JSONP 和 CORS

上面的原因就是跨域错误,一般主流的解决办法有两个

1、 JSONP:全称 JSON with Padding

JSONP 是一种 非正式传输协议(利用早期制定同源策略的 “漏洞”),该协议的一个要点就是允许用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

原理:就是利用 <script> 标签没有跨域限制的 “漏洞” 来达到与第三方通讯的目的,用法就是客户端传过去一个函数名,服务端收到这个函数名再和数据进行拼接形成一个函数调用


2、 CORS: 全称 Cross-Origin Resource Sharing

即:跨来源资源共享。它是一份浏览器技术的规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略,JSONP模式的现代版

具体原理看下面


简单来说,就是浏览器匹配请求头和响应头,如果符合要求便可拿到数据,否则无法拿到数据。整个过程都是由浏览器自动完成。这就像一个白名单,代表着谁可以拿到数据。

服务端是如何支持的?

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。

50205846-accac300-03a4-11e9-8654-2d646d237820.png

服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

所以 CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

简单请求

不会触发 CORS 预检的请求称为简单请求,满足以下 所有条件 的才会被视为简单请求,基本上我们日常开发只会关注前面两点

使用 GETPOSTHEAD 其中一种方法 只使用了如下的安全首部字段,不得人为设置其他首部字段

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type 仅限以下三种
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
  • 请求中没有使用 ReadableStream 对象

非简单请求

顾名思义,只要是会触发 CORS 预检的请求就是非简单请求,其在请求前会先发送一个预检请求(preflight request)

需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响

下面的请求会触发预检请求,其实非简单请求之外的就会触发预检,就不用记那么多了

使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 方法 人为设置了非规定内的其他首部字段,参考上面简单请求的安全字段集合,还要特别注意 Content-Type 的类型 XMLHttpRequestUpload 对象注册了任何事件监听器 请求中使用了 ReadableStream 对象

发起一次 OPTIONS 请求,会带上下面三个 headers

  • Origin:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个 header,服务器将不会进行 CORS 验证。
  • Access-Control-Request-Method:值为实际请求将会使用的方法
  • Access-Control-Request-Headers:值为实际请求将会使用的 header 集合

如果服务器端 CORS 验证失败,则会返回客户端错误,即 4xx 的状态码。

如果请求成功,返回 200 的状态码,并在响应中带上下面这些 headers

  • Access-Control-Allow-Origin:允许请求的域,多数情况下,就是预检请求中的 Origin 的值
  • Access-Control-Allow-Credentials:一个布尔值,表示服务器是否允许使用 cookies
  • Access-Control-Expose-Headers:实际请求中可以出现在响应中的 headers 集合
  • Access-Control-Max-Age:预检请求返回的规则可以被缓存的最长时间,超过这个时间,需要再次发起预检请求
  • Access-Control-Allow-Methods:实际请求中可以使用到的方法集合 浏览器会根据预检请求的响应,来决定是否发起实际请求。

以下是一个发起预检请求的例子 发起请求的 origin 与请求的服务器的 host 不同,而且根据上面的条件判断,触发了预检

50205862-b6542b00-03a4-11e9-8101-a3d8523baa02.png 50205868-ba804880-03a4-11e9-95d9-d5f4df2816b0.png

请求附带 cookies

如果发起请求时设置 withCredentials 标志设置为 true,从而向服务器发送 cookie, 但是如果服务器端的响应中未携带Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者

对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为 *, 必须是某个具体的域名

注意,简单 GET 请求不会被预检;如果对此类带有身份凭证请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页

完整的流程

50205881-c409b080-03a4-11e9-8a57-a2a6d0e1d879.png

SpringBoot配置

参考资料 详解SpringBoot应用跨域访问解决方案

注意:使用了 AllowCredentials 为 true 之后不允许设置 AllowedOrigin*

CorsFilter 全局配置

@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {

CorsConfiguration config = new CorsConfiguration();
//开放哪些ip、端口、域名的访问权限,星号表示开放所有域
// 升级 springboot2.4.0 后, allowedOrigin 不能用通配符 *
config.addAllowedOrigin("http://127.0.0.1:5500");
//是否允许发送Cookie信息
// 注意这个为 true 后下面的 addAllowedHeader 不能为 *
config.setAllowCredentials(false);
//开放哪些Http方法,允许跨域访问 全开用 config.addAllowedMethod("*");
config.addAllowedMethod(HttpMethod.GET);
config.addAllowedMethod(HttpMethod.POST);
config.addAllowedMethod(HttpMethod.PUT);
config.addAllowedMethod(HttpMethod.DELETE);
//允许HTTP请求中的携带哪些Header信息
config.addAllowedHeader("*");
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// 这里必须一个个添加(居然不能再用 * 了,淦)
config.addExposedHeader("Content-Type");
config.addExposedHeader( "X-Requested-With");
config.addExposedHeader("accept");
config.addExposedHeader("Origin");
config.addExposedHeader( "Access-Control-Request-Method");
config.addExposedHeader("Access-Control-Request-Headers");
//添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);

return new CorsFilter(configSource);
}
}

addCorsMappings 方法配置

这个也是全局配置,实现 WebMvcConfigurer 接口的 addCorsMappings 方法,本质就是一个拦截器

@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// // 添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
registry.addMapping("/**")
// 开放哪些ip、端口、域名的访问权限
// 例如:.allowedOrigins("http://127.0.0.1:5500")(用逗号分隔多个)
// 升级 springboot2.4.0 后, allowedOrigin 不能用通配符 *
.allowedOrigins("http://127.0.0.1:5500")
// 是否允许发送Cookie信息
.allowCredentials(true)
// 开放哪些Http方法,允许跨域访问 * 表示全部
.allowedMethods("GET","POST", "PUT", "DELETE")
// 允许HTTP请求中的携带哪些Header信息
.allowedHeaders("*")
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// 这里必须一个个添加
.exposedHeaders(
"Content-Type",
"X-Requested-With",
"accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Allow-Origin",
"Access-Control-Request-Headers");
}
};
}
}

CrossOrigin注解

  • @CrossOrigin 注解加在 Controller 层的方法上,该方法定义的 RequestMapping 端点将支持跨域访问
  • @CrossOrigin 注解加在 Controller 层的类定义处,整个类所有的方法对应的 RequestMapping 端点都将支持跨域访问
@RequestMapping("/cors")
@ResponseBody
@CrossOrigin(origins = "http://localhost:8080", maxAge = 3600)
public String cors( ){
return "cors";
}

使用原生过滤器

@Component
public class CORSFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("Access-Control-Allow-Credentials", "true");
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
response.getWriter().println("ok");
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}

配置测试环境

使用 Vue 快速搭建一个异步请求的环境

<div id="app">
<input type="number" v-model="number">
<button @click="commit">提交测试</button>
</div id>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
let vm = new Vue({
el: "#app",
data: {
number: 0
},
methods: {
commit: function () {
let that = this;
axios.get('http://localhost:8080/hello/temp3',{
params: {
id: that.number
}
})
.then(res =>{
console.log(res.data);
})
}
},
})
</script>

Spring Security 配置

在引入了 Spring Security 之后,我们会发现前面的方法都不能正确的配置 CORS,每次 preflight request 都会得到一个 401 的状态码,表示请求没有被授权。这时,我们需要增加一点配置才能让 CORS 正常工作:

下面这些原因,导致了 preflight request 无法通过身份验证,从而导致 CORS 失效:

  • preflight request 不会携带认证信息
  • Spring Security 通过 Filter 来进行身份验证
  • Interceptor 和 HttpRequestHanlderDispatcherServlet 之后被调用
  • Spring Security 中的 Filter 优先级比我们注入的 CorsFilter 优先级高

Filter 与 Interceptor

在学习这些配置 CORS 之前,先复习一下 FilterInterceptor 的概念。

首先是过滤器和拦截器的执行时间,一个作用在 DispatcherServlet 调用前,一个作用在调用后。

1460000019485886.png

FilterServlet 标准定义,要求 Filter 需要在 Servlet 被调用之前调用;在 Spring Web 应用中,DispatcherServlet 就是唯一的 Servlet 实现。

Interceptor 由 Spring 自己定义,由 DispatcherServlet 调用,可以定义在 Handler 调用前后的行为。这里的 Handler ,在多数情况下,就是我们的 Controller 中对应的方法。

addCorsMappings 注册原理

WebMvcConfigurer.addCorsMappings 方法做了什么?

我们从 WebMvcConfigurer.addCorsMappings 方法的参数开始,先看看 CORS 配置是如何保存到 Spring 上下文中的,然后在了解一下 Spring 是如何使用的它们。

default void addCorsMappings(CorsRegistry registry) {}

首先就是这个传入的参数 CorsRegistry,这个参数用于注册 CORS 的配置

public class CorsRegistry {

private final List<CorsRegistration> registrations = new ArrayList<>();

// 接收一个 pathPattern,创建一个 CorsRegistration 实例,保存到列表后将其返回。
// 例如上面的配置,这里的 pathPattern 就是 /**
public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}

// 方法将保存的 CORS 规则转换成 Map 后返回
protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}

继续下一层,这个 addMapping 方法的返回类型 CorsRegistration(这里只展示部分源码)

public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config; // 保存的一个 pathPattern 对应的 CORS 规则

....

public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
/**
* 这个 applyPermitDefaultValues() 方法就是配置默认的 CORS 规则
*
* allowedOrigins 默认为所有域
* allowedMethods 默认为 GET 、HEAD 和 POST
* allowedHeaders 默认为所有
* maxAge 默认为 30 分钟
* exposedHeaders 默认为 null,也就是不暴露任何 header
* credentials 默认为 null
*
* 创建好这个 CorsRegistration 实例后可以通过它的 allowedOrigins、
* allowedMethods 等方法修改它的 CorsConfiguration,覆盖掉上面的
* 默认值
*/
this.config = new CorsConfiguration().applyPermitDefaultValues();
}

public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
}

现在,我们已经通过 WebMvcConfigurer.addCorsMappings 方法配置好 CorsRegistry

如何注册到 Security 里

上面的都是使用 Interceptor 注册 CORES 的原理,如果只是拿了就用,则直接使用 HtttpSecurity.cors() 方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter() {
protected void configure(HttpSecurity http) throws Exception {
// 使用这个方法把前面注册的 CorsFilter Bean 加入 Filter
http.cors();
}
}

手动跨域

对于实在用不了全局跨域的方法可以手动跨域

// 在响应头上加上

// 允许跨域
resp.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
resp.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");

@CrossOrigin 跨域

controller 方法的 CORS配置,可以向 @RequestMapping 注解处理程序方法添加一个 @CrossOrigin 注解,以便启用 CORS(默认情况下,@CrossOrigin 允许在 @RequestMapping 注解中指定的所有源和 HTTP 方法):

// 或者直接加在类上全局跨域
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

其中 @CrossOrigin 中的 2个参数:

  • origins :允许可访问的域列表
  • maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。

为整个 controller 启用 @CrossOrigin

@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

在这个例子中,对于 retrieve()remove() 处理方法都启用了跨域支持,还可以看到如何使用 @CrossOrigin 属性定制 CORS 配置。

全局 CORS 配置

除了细粒度、基于注释的配置之外,还可能需要定义一些全局 CORS 配置。这类似于使用筛选器,但可以声明为 Spring MVC 并结合细粒度 @CrossOrigin 配置。默认情况下,所有 origins and GET, HEAD and POST methods 是允许的。

JavaConfig,使整个应用程序的 CORS 简化为:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}

如果正在使用 Spring Boot,建议将 WebMvcConfigurer bean 声明如下:

@Configuration
public class MyConfiguration {

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}

可以轻松地更改任何属性,以及仅将此 CORS 配置应用到特定的路径模式:

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}

总结

  • 注入 CorsFilter 的方式会让 CORS 验证在 Filter 中生效
  • 引入 Spring Security 后,需要调用 HttpSecurity.cors 方法以保证 CorsFilter 会在身份验证相关的 Filter 之前执行
  • HttpSecurity.cors + WebMvcConfigurer.addCorsMappings 是一种相对低效的方式,会导致跨域请求分别在 FilterInterceptor 层各经历一次 CORS 验证
  • HttpSecurity.cors + 注册 CorsFilterHttpSecurity.cors + 注册 CorsConfigurationSource 在运行的时候是等效的
  • 在 Spring 中,没有通过 CORS 验证的请求会得到状态码为 403 的响应

Ajax 请求

默认跨域请求是不携带 cookie 的,所以需要进行设置 这里只讲 axios 的设置方法

axios.interceptors.request.use(config => {
config.withCredentials = true;
return config;
});

这样每次请求就会携带上 Cookie 了(注意,后端也要进行相应的设置,看上面)

无法携带 Cookie的问题

突然发现进行了上面的设置依旧无法使用 Cookie,这是因为浏览器一个叫做 SameSite 的设置搞得鬼(气死我了,弄了半天还以为是我同源策略的问题!!!直到看到了这个新特性才发现不是自己的问题,淦!!)

但是暂时还没有看到很好的解决方案,如果只是测试一些其它工具而不是生产环境下,那可以设置在浏览器的 edge://flags/ 暂时关闭这个设置 SameSite by default cookies 改成 Disable

待更新...

References

MDN-跨源资源共享(CORS) 不要再问我跨域的问题了 浅谈 CSRF 攻击方式 Spring 里那么多种 CORS 的配置方式,到底有什么区别 CORS 简单请求+预检请求(彻底理解跨域) 阮一峰-Cookie 的 SameSite 属性 谷歌浏览器新版本Chrome 80默认SameSite导致跨域登录状态失效的问题 浏览器系列之 Cookie 和 SameSite 属性 应对浏览器Cookie新属性sameSite的临门一脚 springboot解决谷歌80及以上版本的SameSite设置cookie失效 Spring Boot中的跨域,为什么加了 Spring Security 就失效了呢? Spring 里那么多种 CORS 的配置方式,到底有什么区别